package com.atguigu.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;
import com.atguigu.gulimall.product.dao.CategoryDao;
import com.atguigu.gulimall.product.entity.CategoryEntity;
import com.atguigu.gulimall.product.service.CategoryBrandRelationService;
import com.atguigu.gulimall.product.service.CategoryService;
import com.atguigu.gulimall.product.vo.Catelog2Vo;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    RedissonClient redissonClient;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        // 查询所有分类
        List<CategoryEntity> categoryEntities = baseMapper.selectList(null);

        // 组装父子结构
        // 找到所有的一级分类 parent_cid =0,并找到子菜单
        List<CategoryEntity> level1Menus = categoryEntities.stream()
                .filter((c) -> c.getParentCid() == 0)
                .map((c) -> {
                    c.setChildren(getChildrens(c, categoryEntities));
                    return c;
                })
                .sorted((menu1, menu2) -> (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort()))
                .collect(Collectors.toList());


        // 找到level1 的子分类
        return level1Menus;
    }

    @Override
    public void removeMenuByIds(List<Long> asList) {
        // TODO 先检查是否可以删除
        baseMapper.deleteBatchIds(asList);
    }

    /**
     * 分类路径
     *
     * @param cateLogId
     * @return
     */
    @Override
    public Long[] findParentPath(Long cateLogId) {
        ArrayList<Long> al = new ArrayList<>();
        ArrayList<Long> cateLogPath = findCateLogPath(cateLogId, al);
        Collections.reverse(cateLogPath);
        return cateLogPath.toArray(new Long[cateLogPath.size()]);
    }

    /**
     * 级联更新所有关联的数据
     *
     * @param category
     * @CacheEvict : 失效模式
     * 1.同时进行多种缓存操作
     * 2.指定删除某个分区的所有数据
     * 3. 规定：存储同一类型的数据，都可以指定成同一个分区！分区名默认就是缓存的名字
     */
    @Transactional
    @Override
//    @Caching(evict = {@CacheEvict(cacheNames = {"category"}, key = "'getLevel1Category'"),
//            @CacheEvict(cacheNames = {"category"}, key = "'getCatalogJson'")}
//    )
    @CacheEvict(cacheNames = {"category"}, allEntries = true) // 失效模式
//    @CachePut // 双写模式
    public void updateCascade(CategoryEntity category) {

        baseMapper.updateById(category);
        if (!StringUtils.isEmpty(category.getName())) {
            categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
        }
        //  TODO 更新其他关联
        // 同时修改缓存中的数据
        // redis.del("catalogJSON"); 等下次主动查询进行更新
    }

    /**
     * 1. 每一个需要缓存的数据我们都来指定放到哪个名字的缓存。【缓存的分区（按照业务类型划分）】
     * 2. @Cacheable({"category"})
     * 代表当前方法的结果需要缓存，如果缓存中有，方法不用调用。如果缓存中有，会将方法的返回值存入缓存，并返回结果
     * category::SimpleKey []
     * <p>
     * 3. 默认行为
     * 1）、如果缓存中有，方法不会调用
     * 2）、key默认自动生成，缓存的名字::SimpleKey[] (自动生成的key值)
     * 3）、缓存的value的值，默认使用jdk序列化机制，将序列化后的数据存到redis中
     * 4）、默认ttl时间-1，即永不过期
     * <p>
     * 自定义：
     * 1）、指定生成的缓存的key：key属性指定。接受一个spel，如果想使用字符串就使用 'xxx'
     * spel 语法 https://docs.spring.io/spring/docs/5.2.7.RELEASE/spring-framework-reference/integration.html#cache
     * 2）、指定缓存的数据存活时间：配置文件中修改
     * 3）、将数据保存为json格式
     * <p>
     * 4.Spring-Cache的不足：
     * 1. 读模式：
     * 缓存穿透，查询一个null数据。解决：缓存空数据：cache-null-value=true
     * 缓存击穿：大量并发进来同时查询一个正好过期的数据。解决：加锁。Spring-Cache 默认是不加锁的(设置 sync = true，这个只是本地锁)
     * 缓存雪崩：大量的key同时过期。解决：加随机事件。加上过期时间。spring.cache.redis.time-to-live=3600000
     * <p>
     * 2.写模式（缓存与数据库一致）
     * 读写加锁。
     * 引入Canal，感知MySql的更新去更新数据库
     * 读多写多，直接去数据库查询就行，别加缓存了。
     * <p>
     * 总结：
     * 常规数据（读多写少，即时性，一致性不高的数据）完全可以使用Spring-Cache。写模式（只要缓存的数据有过期时间就足够了）
     * 特殊数据，特殊设计。
     * <p>
     * 原理：
     * CacheManager(RedisCacheManager) -> Cache(RedisCache)-> Cache 负责缓存的读写
     *
     * @return
     */
    @Cacheable(cacheNames = {"category"},/*key = "'getLevel1Category'"*/key = "#root.methodName", sync = true)
    @Override
    public List<CategoryEntity> getLevel1Category() {
        System.out.println("getLevel1Category......");
        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0L));
    }

    @Cacheable(cacheNames = {"category"}, key = "#root.methodName")
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
        Map<String, List<Catelog2Vo>> collect2 = null;

        System.out.println("查询了数据库...");

        List<CategoryEntity> categoryEntities = baseMapper.selectList(null);
        categoryEntities.stream().filter(l1 -> l1.getParentCid() == 0L).collect(Collectors.toList());

        // 查询一级分类
//        List<CategoryEntity> level1Category = this.getLevel1Category();
        List<CategoryEntity> level1Category = categoryEntities.stream().filter(l1 -> l1.getParentCid() == 0L).collect(Collectors.toList());

        // 封装数据
        if (level1Category != null) {
            collect2 = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), l1 -> {
                // 2. 根据一级分类查询二级分类

                List<CategoryEntity> parent_cid = getParent_cid(categoryEntities, l1.getCatId());
//                List<CategoryEntity> parent_cid = categoryEntities.stream().filter(l2 ->
//                        l2.getParentCid().equals(l1.getCatId())
//                ).collect(Collectors.toList());

                List<Catelog2Vo> collect = null;
                if (parent_cid != null) {
                    collect = parent_cid.stream()
                            .map(l2 -> {
                                Catelog2Vo catelog2Vo = new Catelog2Vo(l1.getCatId().toString(), l2.getCatId().toString(), l2.getName(), null);

                                //  根据二级分类查询三及分类
                                List<CategoryEntity> parent_cid1 = getParent_cid(categoryEntities, l2.getCatId());
//                                List<CategoryEntity> parent_cid1 = categoryEntities.stream().filter(l3 ->
//                                        l3.getParentCid().equals(l2.getCatId())
//                                ).collect(Collectors.toList());
                                List<Catelog2Vo.Catelog3Vo> collect1 = null;
                                if (parent_cid1 != null) {
                                    collect1 = parent_cid1.stream().map(l3 -> {
                                        Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString()
                                                , l3.getName());
                                        return catelog3Vo;
                                    }).collect(Collectors.toList());
                                }
                                // 将查询的3及分类封装给2个分类
                                catelog2Vo.setCatalog3List(collect1);
                                return catelog2Vo;
                            }).collect(Collectors.toList());
                    return collect;
                }
                return collect;
            }));
        }

        return collect2;
    }

    // TODO 产生堆外内存溢出：OutOfDirectMemoryError
    // 1)、springboot2.0 以后默认使用lettuce 作为操作redis的客户端。lettuce 使用netty进行网络通信。
    // 2)、但是lettuce一直存在bug导致netty堆外内存溢出。而netty如果没有指定堆外内存默认使用jvm参数-Xmx300m
    // 我们也可以通过-Dio.netty.maxDirectMemory进行设置，
    // 解决方案：不能使用 去调大 -Dio.netty.maxDirectMemory 堆外内存，只能修改lettuce源码
    // 1)、设计lettuce客户端（改源码）。2）、切换使用jedis（缺点很久没更新了）
    // redisTemplate: 是对redis各个客户端的同一API，方便我们程序员使用
    // lettuce、jedis 是操作redis的底层客户端。Spring再次封装成redisTemplate
    public Map<String, List<Catelog2Vo>> getCatalogJson2() {
        /**
         * 1、 空结果缓存：解决缓存穿透
         * 2、 设置过期时间（加随机值）：解决缓存雪崩
         * 3、 加锁：解决缓存击穿
         */
        // 从缓存获取分类数据
        String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
        Map<String, List<Catelog2Vo>> catalogs = null;
        if (StringUtils.isEmpty(catalogJSON)) {
            // 缓存没有数据，查询数据库
            System.out.println("缓存不命中....查询数据库....");
            catalogs = getCatalogJsonFromDBWithRedisLock();
            // 将查询的结果存入缓存,序列化对象为json字符串,有效期是一周（expire）
//            stringRedisTemplate.opsForValue().set("catalogJSON", JSON.toJSONString(catalogs));
//            stringRedisTemplate.opsForValue().set("catalogJSON", JSON.toJSONString(catalogs), 1, TimeUnit.DAYS);// 缓存一天
            return catalogs;
        }
        // 将json 数据反序列化为对象
        catalogs = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        System.out.println("缓存命中......直接返回....");
        return catalogs;

    }

    // 使用redisson 分布式锁解决分布式缓存问题
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithRedissonLock() {
        RLock lock = redissonClient.getLock("CatalogJson-lock");
        lock.lock();
        try {
            // 获取数据，不过获取的结果最后还是要删锁
            Map<String, List<Catelog2Vo>> data = getData();
            return data; // 会执行完finally的代码在return
        } finally {
            // 解锁
            lock.unlock();
        }

    }

    // TODO 从数据库查询并封装分类数据，分布式锁（原子上锁，原子解锁）
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithRedisLock() {
        // 占分布式锁。去redis占坑
        String uuid = UUID.randomUUID().toString();
        // 为了防止业务超时，我们可以把过过期时间设置的长一点，保证只能是被删除的而不是由于有效期结束了而被删除
        // 设置有效期过长的缺点是，如果在删锁过程服务器宕机或者抛出异常，这个锁就不能被删掉，所以在一定时间内服务室没法提供的。
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);// set lock 11 EX 300 NX
        if (lock) {
            // 设置成功，占到坑
            try {
                // 获取数据，不过获取的结果最后还是要删锁
                Map<String, List<Catelog2Vo>> data = getData();
                return data; // 会执行完finally的代码在return
            } finally {
                // 判断是不是自己的锁在解锁，判断和删除锁必须是原子的
                String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end";
                // 泛型是为了序列化结果，这是官方建议的，分布式上锁，最好使用lua脚本来执行
                Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
            }

        } else {
            try {
                Thread.sleep(100); // 失败就稍等一下，在重试。
            } catch (Exception e) {
            }
            return this.getCatalogJsonFromDBWithRedisLock(); // 自旋
        }
    }

    public Map<String, List<Catelog2Vo>> getData() {
        // 再次查看缓存有没有数据
        String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
        if (!StringUtils.isEmpty(catalogJSON)) {
            // 上一个拿到锁的线程已经给缓存放入了东西
            return JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
        } else {
            Map<String, List<Catelog2Vo>> collect2 = null;

            System.out.println("查询了数据库...");

            List<CategoryEntity> categoryEntities = baseMapper.selectList(null);
            categoryEntities.stream().filter(l1 -> l1.getParentCid() == 0L).collect(Collectors.toList());

            // 查询一级分类
//        List<CategoryEntity> level1Category = this.getLevel1Category();
            List<CategoryEntity> level1Category = categoryEntities.stream().filter(l1 -> l1.getParentCid() == 0L).collect(Collectors.toList());

            // 封装数据
            if (level1Category != null) {
                collect2 = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), l1 -> {
                    // 2. 根据一级分类查询二级分类

                    List<CategoryEntity> parent_cid = getParent_cid(categoryEntities, l1.getCatId());
//                List<CategoryEntity> parent_cid = categoryEntities.stream().filter(l2 ->
//                        l2.getParentCid().equals(l1.getCatId())
//                ).collect(Collectors.toList());

                    List<Catelog2Vo> collect = null;
                    if (parent_cid != null) {
                        collect = parent_cid.stream()
                                .map(l2 -> {
                                    Catelog2Vo catelog2Vo = new Catelog2Vo(l1.getCatId().toString(), l2.getCatId().toString(), l2.getName(), null);

                                    //  根据二级分类查询三及分类
                                    List<CategoryEntity> parent_cid1 = getParent_cid(categoryEntities, l2.getCatId());
//                                List<CategoryEntity> parent_cid1 = categoryEntities.stream().filter(l3 ->
//                                        l3.getParentCid().equals(l2.getCatId())
//                                ).collect(Collectors.toList());
                                    List<Catelog2Vo.Catelog3Vo> collect1 = null;
                                    if (parent_cid1 != null) {
                                        collect1 = parent_cid1.stream().map(l3 -> {
                                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString()
                                                    , l3.getName());
                                            return catelog3Vo;
                                        }).collect(Collectors.toList());
                                    }
                                    // 将查询的3及分类封装给2个分类
                                    catelog2Vo.setCatalog3List(collect1);
                                    return catelog2Vo;
                                }).collect(Collectors.toList());
                        return collect;
                    }
                    return collect;
                }));
            }
            // 上一次拿到锁的线程给缓存放入了数据。
            stringRedisTemplate.opsForValue().set("catalogJSON", JSON.toJSONString(collect2), 1, TimeUnit.DAYS);// 缓存一天

            return collect2;
        }
    }

    // TODO 从数据库查询并封装分类数据，本地锁
    public /*synchronized*/ Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithLocalLock() {
        // 只要是同一把锁，就能锁住这个锁的所有线程。
        // synchronized (this)；SpringBoot 所有的组件在容器中都是单例的，所以同一时刻只有一个线程能访问这个方法，synchronize放在这里是可以实现业务要求的。
        // TODO 本地锁：synchronize，JUS（lock），在分布式情况下，想要锁住所有，必须使用分布式锁（分布式锁缺点效率慢）
        synchronized (this) {
            /**
             * 将数据库的多次查询变为一次
             */
            String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
            Map<String, List<Catelog2Vo>> collect2 = null;
            if (StringUtils.isEmpty(catalogJSON)) {
                collect2 = getData();
            }

            return collect2;
        }
    }

    private List<CategoryEntity> getParent_cid(List<CategoryEntity> categoryEntities, Long catId) {
        return categoryEntities.stream().filter(l -> l.getParentCid().equals(catId)).collect(Collectors.toList());
//        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", catId));
    }

    private ArrayList<Long> findCateLogPath(Long cateLogId, ArrayList<Long> al) {
        // 获取当前分类的信息
        CategoryEntity category = this.baseMapper.selectById(cateLogId);
        if (category != null) {
            al.add(cateLogId);
            findCateLogPath(category.getParentCid(), al);
        }
        return al;
    }

    //递归查找所有菜单的子菜单
    private List<CategoryEntity> getChildrens(CategoryEntity parent, List<CategoryEntity> all) {
        List<CategoryEntity> list = all.stream()
                // 找到子菜单
                .filter((c) -> c.getParentCid().equals(parent.getCatId()))
                .map((c) -> {
                    c.setChildren(getChildrens(c, all));
                    return c;
                })
                // 排序
                .sorted((menu1, menu2) -> (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort()))
                .collect(Collectors.toList());

        return list;
    }
}